package de.hauschild.orbitvm.vm;

import java.util.ArrayList;
import java.util.List;

import de.hauschild.orbitvm.vm.instruction.Instruction;

/**
 * The Orbit virtual machine.
 * 
 * @since 1.0.0
 * @author Klaus Hauschild
 */
public class VM {

  private static final int PROBLEM_CONFIGURATION_INPUT_PORT = 0x3E80;
  private static final double EARTH_RADIUS = 6357000.0;
  private static final int MAX_MEMORY_ADRESS = 16384;

  /**
   * Create new double array with given size and assign <code>0.0</code> to each slot.
   * 
   * @param size
   *          the size
   * @return the double[]
   */
  private static Double[] newDoubleArray(final int size) {
    final Double[] array = new Double[size];
    for (int i = 0; i < size; i++) {
      array[i] = 0.0;
    }
    return array;
  }

  private final Double[] memory;
  private final Double[] input;
  private final Double[] output;
  private final List<Instruction> instructions;
  private boolean status;
  private int iterationCounter;
  private Double velocityImpulseX = 0.0;
  private Double velocityImpulseY = 0.0;

  /**
   * Instantiates a new {@link VM}.
   * 
   * @param initialMemory
   *          the initial memory
   * @param theInstructions
   *          the instructions
   */
  public VM(final Double[] initialMemory, final List<Instruction> theInstructions) {
    memory = newDoubleArray(MAX_MEMORY_ADRESS);
    for (int i = 0; i < initialMemory.length; i++) {
      memory[i] = initialMemory[i];
    }
    input = newDoubleArray(MAX_MEMORY_ADRESS);
    output = newDoubleArray(MAX_MEMORY_ADRESS);
    instructions = new ArrayList<Instruction>(theInstructions);
  }

  /**
   * Gets the distance to earth.
   * 
   * @return the distance to earth
   */
  public Double getDistanceToEarth() {
    return getSatellitePosition().length();
  }

  /**
   * Gets the input.
   * 
   * @return the input
   */
  public Double[] getInput() {
    return input;
  }

  /**
   * Gets the instructions.
   * 
   * @return the instructions
   */
  public List<Instruction> getInstructions() {
    return instructions;
  }

  /**
   * Gets the memory.
   * 
   * @return the memory
   */
  public Double[] getMemory() {
    return memory;
  }

  /**
   * Gets the output.
   * 
   * @return the output
   */
  public Double[] getOutput() {
    return output;
  }

  /**
   * Gets the remaining fuel.
   * 
   * @return the remaining fuel
   */
  public Double getRemainingFuel() {
    return output[1];
  }

  /**
   * Gets the satellite position.
   * 
   * @return the satellite position
   */
  public DoublePoint getSatellitePosition() {
    return new DoublePoint(output[2], output[3]);
  }

  /**
   * Gets the score.
   * 
   * @return the score
   */
  public Double getScore() {
    return output[0];
  }

  /**
   * Gets the status.
   * 
   * @return the status
   */
  public boolean getStatus() {
    return status;
  }

  /**
   * Iterate the virtual machine.
   * 
   * @return the iteration counter
   * @throws VMException
   *           the VM exception
   */
  public int iterate() throws VMException {
    // velocity impulse
    setVelocityDelta(new DoublePoint(velocityImpulseX, velocityImpulseY));
    for (final Instruction instruction : instructions) {
      instruction.evaluate(this);
    }
    // machine not initialized
    if (getScore() == -1.0) {
      throw new VMException("Problem code not set!", true);
    }
    // earth crash
    if (getDistanceToEarth() <= EARTH_RADIUS) {
      throw new VMException("Satelite crashes into earth!", true);
    }
    if (getRemainingFuel() <= 0.0) {
      throw new VMException("Out of fuel, lost in space!", true);
    }
    // get score -> exit
    if (getScore() > 0.0) {
      throw new VMException(String.format("Mission complete! Score: %.2f.", getScore()), false);
    }
    iterationCounter++;
    // reset velocity impulse
    setVelocityDelta(new DoublePoint(0.0, 0.0));
    velocityImpulseX = 0.0;
    velocityImpulseY = 0.0;
    return iterationCounter;
  }

  /**
   * Sets the configuration.
   * 
   * @param problemCode
   *          the problem code
   */
  public void setConfiguration(final int problemCode) {
    input[PROBLEM_CONFIGURATION_INPUT_PORT] = (double) problemCode;
  }

  /**
   * Sets the status.
   * 
   * @param theStatus
   *          the new status
   */
  public void setStatus(final boolean theStatus) {
    status = theStatus;
  }

  /**
   * Sets the velocity delta.
   * 
   * @param deltaV
   *          the new velocity delta
   */
  public void setVelocityDelta(final DoublePoint deltaV) {
    input[2] = deltaV.getX();
    input[3] = deltaV.getY();
  }

  /**
   * Sets the velocity impulse.
   * 
   * @param velocityImpulse
   *          the new velocity impulse
   */
  public void setVelocityImpulse(final DoublePoint velocityImpulse) {
    velocityImpulseX = velocityImpulse.getX();
    velocityImpulseY = velocityImpulse.getY();
  }
}
